// TODO: CODE REVIEW & CLEANUP!
//
// SunfireSimulator.cs
//
// Implements HomeUX.Components.SunfireSimulator.
//

using System;
using System.IO.Ports;
using System.Text;
using System.Text.RegularExpressions;
using HomeUX.Utilities;

namespace HomeUX.Components
{

/// <summary>
/// Simulates a Sunfire Theater Grand Processor II.
/// </summary>
///
/// <remarks>
/// The following constructor arguments are supported (see <r>DnUtil.CreateInstance</r>):
/// <list type="table">
///     <listheader>
///         <term>Argument</term>
///         <description>Description</description>
///     </listheader>
///     <item>
///         <term><b>Test</b></term>
///         <description>(For development use only.)  The ID of the test to execute.  The default
///             is 0, indicating no test.</description>
///     </item>
/// </list>
/// <remarks>
///
public class SunfireSimulator : VirtualSerialPort
{
    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Private Constants
    //

    /// <summary>
    /// The number of milliseconds to wait before each reply is sent back to the serial port.
    /// Increasing this number slows down the speed of the simulation.
    /// </summary>
    const int LatencyMsec = 1000;

	/// <summary>
	/// Matches an "absolute volume" command.
	/// </summary>
	static readonly Regex VolumeAbsoluteRegex = new Regex(@"^\*11U([0-9][0-9])$");

    ///////////////////////////////////////////////////////////////////////////////////////////////
	// Private State
	//

	/// <summary>
	/// This object is locked before a thread accesses this object, to serialize access to the
	/// object.
	/// </summary>
	object _lock = new object();

	/// <summary>
	/// Serial port read buffer, i.e. data to send to client.
	/// </summary>
	StringBuilder _sendToClient = new StringBuilder(1000);

	/// <summary>
	/// Work queue for scheduling asynchronous operations.
	/// </summary>
	WorkQueue _workQueue;

    /// <summary>
    /// Identifies a test being performed using this simulator, or 0 if no test is being
    /// performed.
    /// </summary>
    int _test;

    /// <summary>
    /// The number of commands that have been executed.
    /// </summary>
    int _commandsExecuted;

	///////////////////////////////////////////////////////////////////////////////////////////////
	// Public Methods
	//

	/// <summary>
	/// Creates an instance of this class.
	/// </summary>
	///
	public SunfireSimulator() : this(new string[0])
	{
	}

    public SunfireSimulator(string[] args)
    {
        // parse <args>
        foreach (string arg in args)
        {
            try
            {
                string[] argParts = arg.Split('=');
                if (argParts.Length != 2)
                    throw new ArgumentException();
                string argName = argParts[0];
                string argValue = argParts[1];
                switch (argName)
                {
                case "Test":
                    _test = System.Xml.XmlConvert.ToInt32(argValue);
                    break;
                default:
                    throw new ArgumentException();
                }
            }
            catch (Exception ex)
            {
                if ((ex is FormatException) || (ex is OverflowException) ||
                    (ex is ArgumentException))
                {
                    throw new VirtualSerialPortException("Invalid argument: {0}", arg);
                }
                else
                    throw;
            }
        }
    }

	///////////////////////////////////////////////////////////////////////////////////////////////
	// IDisposable Overrides
	//

    public override void Dispose()
    {
		lock (_lock)
		{
			base.Dispose();
			if (_workQueue != null)
			{
				_workQueue.Dispose();
				_workQueue = null;
			}
		}
    }

	///////////////////////////////////////////////////////////////////////////////////////////////
	// VirtualSerialPort Overrides
	//

	public override bool SupportsLineBasedTextMode
	{
		// this virtual serial port only supports line-based text mode
		get
		{
			return true;
		}
	}

	public override string NewLine
	{
		// note that NewLine refers to the line terminator that the device expects to receive,
		// though in the case of this device they're the same
		get
		{
            return "\r";
		}
        set
        {
            if (value != "\r")
            {
                throw new InvalidOperationException(
					"Sunfire expects lines it receives to be terminated by CR");
			}
        }
	}

    public override void Open()
    {
        lock (_lock)
        {
			// create a work queue for queueing events
			_workQueue = new WorkQueue("SunfireSimulator");

			// debugging feedback
			Trace("Initialized");

            if (_test == 1)
                _sendToClient.Append("RandomJunk\r\nFromSunfireSimulator");
		}
    }

	public override string ReadExisting()
	{
		lock (_lock)
		{
			string text = _sendToClient.ToString();
			_sendToClient.Length = 0;
			return text;
		}
	}

	public override void WriteLine(string text)
	{
		// client sent us a command (<text>)
		lock (_lock)
		{
			Trace("Received command: {0}", text);
			ExecuteSunfireCommand(text);
		}
	}

	///////////////////////////////////////////////////////////////////////////////////////////////
	// Private Properties & Methods
	//

	/// <summary>
	/// Executes a Sunfire command sent from the client.
	/// </summary>
	///
	/// <param name="command">The Sunfire command to execute, not including the terminating
	///     carriage return.  For example, "*112" (Power On).</param>
	///
    void ExecuteSunfireCommand(string command)
    {
        _workQueue.Add(100, delegate
        {
            ExecuteSunfireCommand2(command);
        });
    }
	void ExecuteSunfireCommand2(string command)
	{
		lock (_lock)
		{
			// keep track of the number of commands that have been executed (including this one)
			_commandsExecuted++;

			// echo the command, including a newline
            if ((_test == 1) && (_commandsExecuted == 2))
            {
                // write extra junk after the echo on the second command
                WriteToClient(command + "\rMoreJunkFromSunfireDriver");
            }
            else
            if ((_test == 1) && (_commandsExecuted == 3))
            {
                // cause a driver timeout on the third command by not echoing the first successful command
            }
            else
				WriteToClient(command + "\r");

			// interpret <command>; set <description> to a description of it
			string description;
			Match match = VolumeAbsoluteRegex.Match(command);
			if (match.Success)
			{
				description = String.Format("VOL ABSOLUTE: {0}", match.Groups[1].Value);
			}
			else
			switch (command)
			{
			case "*111": description = "POWER TOGGLE"; break;
			case "*112": description = "POWER ON"; break;
			case "*113": description = "POWER OFF"; break;
			case "*114": description = "CD"; break;
			case "*115": description = "DAT/TAPE"; break;
			case "*116": description = "DBS/BS"; break;
			case "*117": description = "LD/DVD"; break;
			case "*118": description = "PHONO"; break;
			case "*119": description = "TUNER"; break;
			case "*11A": description = "V.AUX"; break;
			case "*11B": description = "VCR1"; break;
			case "*11C": description = "VCR2"; break;
			case "*11D": description = "DSP"; break;
			case "*11E": description = "STEREO"; break;
			case "*11F": description = "PRO LOGIC"; break;
			case "*11G": description = "3-STEREO"; break;
			case "*11H": description = "CATHEDRAL"; break;
			case "*11J": description = "STADIUM"; break;
			case "*11K": description = "JAZZ-CLUB"; break;
			case "*11L": description = "HOLO TOGGLE"; break;
			case "*11M": description = "HOLO ON"; break;
			case "*11N": description = "HOLO OFF"; break;
			case "*11P": description = "MUTE TOGGLE"; break;
			case "*11Q": description = "MUTE ON"; break;
			case "*11R": description = "MUTE OFF"; break;
			case "*11S": description = "VOLUME UP"; break;
			case "*11T": description = "VOLUME DOWN"; break;
			default:     description = "Unknown: " + command; break;
			}

			// trace <description>
            Trace("Command is: {0}", description);
		}
	}

	/// <summary>
	/// Formats a string and sends it string to the client asynchronously.  The client will receive
	/// the string on the virtual serial port.
	/// </summary>
	/// 
	/// <param name="format">The error message format string.  If <pr>args</pr> is empty,
	/// 	<pr>format</pr> is used without formatting.</param>
	///
	/// <param name="args">Formatting arguments.</param>
	///
	void WriteToClient(string format, params object[] args)
	{
		lock (_lock)
		{
			WriteToClient((args.Length == 0) ? format : String.Format(format, args));
		}
	}

	/// <summary>
	/// Sends a string to the client asynchronously.  The client will receive the string on the
	/// virtual serial port.
	/// </summary>
	///
	void WriteToClient(string text)
	{
		lock (_lock)
			_sendToClient.Append(text);
		_workQueue.Add(delegate
		{
            FireDataReceived(SerialData.Chars);
		});
	}

    /// <summary>
    /// Writes a formatted string to the debug output window.
    /// </summary>
    ///
    static void Trace(string format, params object[] args)
    {
        // static -- no lock required
        Util.Trace("SunfireSimulator: {0}", Util.Format(format, args));
    }
}

}

